package ai

import java.util.Collections
import java.util.List
import java.util.Map.Entry

import ai.human.PassKnowBase
import data.Player
import data.Position
import data.Team
import data.Data.BlockActionType
import data.Data.PlayerState
import data.Data.Race
import data.Data.TeamID
import data.Data.Zones
import engine.IGameEngine
import engine.Pitch
import engine.GameEngine.GameState
import gui.PitchGUI
import gui.customControl.LogWindow

/**
 * Ai engine class. 
 * @author Tomasz Dąbrowski
 *
 */
class AIEngine {

	static boolean killFlag = false

	/**
	 * Danger value when enemy is close.
	 */
	static final int NEAR_ENEMY_DANGER_VALUE = 100
	/**
	 * Max danger value when enemy is not close.
	 */
	static final int MAX_DANGER = 10
	/**
	 * Threads lock.
	 */
	static final Object threadsLock = new Object()
	/**
	 * Last thread execution time.
	 */
	static long threadsTime = System.nanoTime()

	/**
	 * Map danger values.
	 */
	Map dangerValues = [:]
	/**
	 * Game engine.
	 */
	IGameEngine gameEngine
	/**
	 * Controlled team.
	 */
	Team team
	/**
	 * Random start positions.
	 */
	List<Position> startPositions
	/**
	 * New round flag.
	 */
	boolean newRound = false
	/**
	 * Previous positions.
	 */
	Map previousPoss = [:]
	/**
	 * Game thread.
	 */
	GameThread gameThread

	/**
	 * Gets the field positions.
	 *
	 * @return
	 *
	 * @author Tomasz Dąbrowski
	 */
	static List pitchRange() {
		[
			0..<Pitch.PITCH_WIDTH,
			0..<Pitch.PITCH_HEIGHT
		].combinations().collect { new Position(it[0], it[1]) }
	}

	/**
	 * Gets positions of the position range.
	 *
	 * @param p position
	 * @return range
	 *
	 * @author Tomasz Dąbrowski
	 */
	static List posRange(Position p, def range = 1, boolean center = false) {
		assert Pitch.isInsidePitch(p)
		List ps = [-range..range, -range..range].combinations().collect {
			new Position((int)(p.x() + it[0]), (int)(p.y() + it[1]))
		}.findAll { Pitch.isInsidePitch(it) }
		assert p in ps
		if(!center) {
			ps.remove(p)
		}
		assert center || !(p in ps)
		ps
	}

	/**
	 * Gets the circle of positions around the position. 
	 *
	 * @param p position
	 * @param range circle range
	 * @return positions
	 *
	 * @author Tomasz Dąbrowski
	 */
	static List posCircle(Position p, int range) {
		pitchRange().findAll { Position pos ->
			pos.distance(p) == range
		}
	}

	/**
	 * Gets the pitch side of the team.
	 *
	 * @param teamID team id
	 * @return pitch side
	 *
	 * @author Tomasz Dąbrowski
	 */
	static List pitchTeam(TeamID teamID) {
		([
			0..<Pitch.PITCH_WIDTH,
			0..<Pitch.PITCH_HEIGHT]
		).combinations().collect { new Position(it[0], it[1]) }.findAll { Position p ->
			teamID == TeamID.TEAM_A ? Pitch.isZoneTeamA(p) : Pitch.isZoneTeamB(p)
		}
	}

	/**
	 * Creates the ai engine instance.
	 *
	 * @param team controlled team
	 * @param gameEngine game engine
	 * @return ai engine instance
	 *
	 * @author Tomasz Dąbrowski
	 */
	static AIEngine createInstance(Team team, IGameEngine gameEngine) {
		AIEngine aiEngine
		switch (team.race) {
			case Race.HUMAN:
				aiEngine = new AIEngineHuman(team, gameEngine)
				break
			case Race.ORC:
				aiEngine = new AIEngine(team, gameEngine);
				break
			default:
				throw new IllegalArgumentException("this race isn't supported yet")
		}
		aiEngine.gameThread = new GameThread(aiEngine : aiEngine)
		aiEngine.gameThread.setDaemon(true)
		aiEngine.gameThread.start()
		aiEngine
	}

	/**
	 * Checks if next round has started.
	 */
	def isNextRound = {
		gameEngine.gameEnd || gameEngine.currentTeamID() != team.id
	}

	/**
	 * Creates new ai engine.
	 *
	 * @param team controlled team
	 * @param gameEngine game engine
	 *
	 * @author Tomasz Dąbrowski
	 */
	public AIEngine(Team team, IGameEngine gameEngine) {
		this.gameEngine = gameEngine
		setTeam(team)
	}


	/**
	 * Gets the player with the ball.
	 *
	 * @return
	 *
	 * @author Tomasz Dąbrowski
	 */
	public Player getPlayerWithBall() {
		assert gameEngine
		(gameEngine.teamA().players() + gameEngine.teamB().players()).find { it.hasBall() }
	}

	/**
	 * Gets the ball position.
	 *
	 * @return ball position
	 *
	 * @author Tomasz Dąbrowski
	 */
	public Position getBallPosition() {
		def ballPos = getPlayerWithBall() ?: gameEngine.getPlayingField().ballPosition
		if(ballPos) {
			new Position(ballPos)
		}
	}

	/**
	 * Checks if the player is from the team.
	 *
	 * @param p player
	 * @param teamID  team id
	 * @return is from team
	 *
	 * @author Tomasz Dąbrowski
	 */
	public boolean isFromTeam(Player p, TeamID teamID) {
		if(teamID == TeamID.TEAM_A) {
			p in gameEngine.teamA().players()
		} else {
			p in gameEngine.teamB().players()
		}
	}

	/**
	 * Evaluates the kickoff position.
	 *
	 * @param p position
	 * @return value
	 *
	 * @author Tomasz Dąbrowski
	 */
	public int evaluateKickOffPosition(Position p) {

		[-3..3, -3..3].combinations().sum {
			switch(new Position(p.x() + it[0], p.y() + it[1])) {
				case { team.id == TeamID.TEAM_A ? !Pitch.isTeamBCenterZone(it) : !Pitch.isTeamACenterZone(it) }:
					-3
					break
				case { gameEngine.getPlayingField().getPlayer(it) }:
					-2
					break
				default:
					0
					break
			}
		}
	}

	/**
	 * Gets a kickoff position.
	 *
	 * @return kickoff position
	 *
	 * @author Tomasz Dąbrowski
	 */
	public Position getKickOffBallPosition() {
		List ps = pitchRange()
		Collections.shuffle(ps)
		ps.collectEntries { Position p ->
			[
				p,
				evaluateKickOffPosition(p)
			]
		}.max { Entry e -> e.value }.key
	}

	/**
	 * Gets a touchback player.
	 *
	 * @return player
	 *
	 * @author Tomasz Dąbrowski
	 */
	protected Player getTouchBackPlayer() {
		List players = team.players().collect { it }
		Collections.shuffle(players)
		players.find { Player p -> p.state() == PlayerState.IN_PLAY }
	}

	/**
	 * Generates random start positions.
	 *
	 * @return positions
	 *
	 * @author Tomasz Dąbrowski
	 */
	private List<Position> getRandomPositions() {
		List ps = []
		switch (team.id) {
			case TeamID.TEAM_A:
				[4, 7, 10].each {
					ps.add(new Position(12, it))
				}
				break
			case TeamID.TEAM_B:
				[4, 7, 10].each {
					ps.add(new Position(13, it))
				}
				break
		}
		List fps = [2..12, 4..10].combinations()
		Collections.shuffle(fps)
		ps.addAll(fps.collect { new Position(it[0], it[1]) })
		ps.sort { Pitch.isScrimmageZone(it) ? 0 : 1 }
		while(Pitch.isScrimmageZone(ps[3])) {
			ps.remove(0)
		}
		ps = ps[0..<Pitch.MAX_PLAYERS_ON_FIELD]
		ps
	}

	/**
	 * Creates player groups - fake implementation.
	 *
	 *
	 * @author Tomasz Dąbrowski
	 */
	public void createGroups() {
	}

	/**
	 * Gets the zone of the team.
	 *
	 * @param zone zone
	 * @return team's zone
	 *
	 * @author Tomasz Dąbrowski
	 */
	protected List<Position> getTeamZone(Zones zone) {
		pitchRange().findAll { Position p ->
			switch (zone) {
				case Zones.SCRIMMAGE_LINE:
					team.getId() == TeamID.TEAM_A ? Pitch.isScrimmageZoneTeamA(p) : Pitch.isScrimmageZoneTeamB(p)
					break
				case Zones.BOTTOM_WIDE:
					team.getId() == TeamID.TEAM_A ? Pitch.isZoneTeamA(p) && Pitch.isBottomWideZone(p) : Pitch.isZoneTeamB(p) && Pitch.isBottomWideZone(p)
					break
				case Zones.TOP_WIDE:
					team.getId() == TeamID.TEAM_A ? Pitch.isZoneTeamA(p) && Pitch.isTopWideZone(p) : Pitch.isZoneTeamB(p) && Pitch.isTopWideZone(p)
					break
				case [
					Zones.LEFT_PITCH,
					Zones.RIGHT_PITCH
				]:
					team.getId() == TeamID.TEAM_A ? Pitch.isTeamACenterZone(p) : Pitch.isTeamBCenterZone(p)
					break
			}
		}
	}

	/**
	 * Is kicking team after the coin toss.
	 *
	 * @return is kicking team
	 *
	 * @author Tomasz Dąbrowski
	 */
	public boolean isKickOffKickingTeam() {
		true
	}

	/**
	 * On team switch method.
	 *
	 * @author Tomasz Dąbrowski
	 */
	public void onSwitchTeam() {
		team.players().findAll { Player p ->
			p.state() == PlayerState.PRONE
		}.each { gameEngine.processStandup(it) }
		PassKnowBase.getInstance(team.id.other())?.commitKB()
		newRound = true
	}

	/**
	 * Check if new round and sets the value to false. 
	 *
	 * @return new round
	 *
	 * @author Tomasz Dąbrowski
	 */
	public boolean getAndSetNewRound() {
		try {
			return newRound
		} finally {
			newRound = false
		}
	}

	/**
	 * Player action - the main class method.
	 *
	 * @return end turn if false
	 *
	 * @author Tomasz Dąbrowski
	 */
	public boolean playerAction() {
		Player p = team.players().find { Player p ->
			p.currentMA() > 0
		}
		if(!p) {
			return false
		}

		Position prevPos = previousPoss[p]
		Position nextPos
		if(prevPos) {
			def x = p.x() - prevPos.x()
			def y = p.y() - prevPos.y()
			nextPos = new Position(p.x() + x, p.y() + y)
		}

		def poss = posRange(new Position(p))
		Collections.shuffle(poss)
		def iter = poss.iterator()


		while(!nextPos || !Pitch.isInsidePitch(nextPos) || gameEngine.playingField.getPlayer(nextPos)) {
			if(!iter.hasNext()) {
				break
			}
			nextPos = iter.next()
		}

		if(!nextPos) {
			p.setCurrentMA(0)
			return true
		}
		prevPos = new Position(p)
		boolean notMoved = !gameEngine.movePlayer(p, nextPos)
		if(notMoved) {
			p.setCurrentMA(0)
		} else {
			previousPoss[p] = prevPos
		}
		true
	}

	/**
	 * Gets the best block type.
	 *
	 * @param blockActions
	 * @return
	 *
	 * @author Tomasz Dąbrowski
	 */
	public BlockActionType getBlockAction(List blockActions) {
		blockActions.sort {
			switch(it) {
				case BlockActionType.DEFENDER_DOWN:
					return 5
				case BlockActionType.DEFENDER_STUMBLES:
					return 4
				case BlockActionType.PUSHED:
					return 3
				case BlockActionType.BOTH_DOWN:
					return 2
				case BlockActionType.ATTACKER_DOWN:
					return 1
			}
		}.last()
	}

	/**
	 * Do follow enemy after push.
	 *
	 * @param player attacking player
	 * @param enemy defending player
	 * @return do follow
	 *
	 * @author Tomasz Dąbrowski
	 */
	public boolean followEnemy(Player player, Player enemy) {
		false
	}

	/**
	 * Gets the pust destination (direction).
	 *
	 * @param player attacking player
	 * @param enemy defending player
	 * @return position to be pushed to
	 *
	 * @author Tomasz Dąbrowski
	 */
	public Position getPushDestination(Player player, Player enemy) {
		Position p = posRange(new Position(player)).findAll {
			!gameEngine.playingField.getPlayer(it)
		}.sort { getDangerValue(it)  }.last()
	}

	/**
	 * Sets a player on pitch.
	 *
	 * @author Tomasz Dąbrowski
	 */
	public Position setPlayer() {
		Player player = team.players().find { it.state() == PlayerState.IN_DUGOUT }

		startPositions = !startPositions ? getRandomPositions() : startPositions
		Position position
		while (true) {
			assert !startPositions.isEmpty()
			position = startPositions.remove(0)
			if (Pitch.isZoneOppositeTeam(position, team) || !gameEngine.playingField.isEmpty(position)) {
				continue
			}
			Player p = gameEngine.getPlayingField().getPlayer(position)
			if(p) {
				continue
			}
			gameEngine.placePlayer(player, position)
			if ( player.state() == PlayerState.IN_PLAY) {
				break
			}
		}
		position
	}

	/**
	 * Calculates the safest source - destination pair.
	 *
	 * @param srcs source
	 * @param dsts destination
	 * @return source - destination pair
	 *
	 * @author Tomasz Dąbrowski
	 */
	public List getSafestDestinationByPath(List<Position> srcs, List<Position> dsts) {
		List pairs = [srcs, dsts].combinations()
		pairs = pairs.collect { List poss ->
			int d = 0
			Position current = poss[0]
			while(!current.isNeighbour(poss[1])) {
				List range = AIEngine.posRange(current)
				double minDist = range.collect { Position pos -> pos.distanceAlt(poss[1]) }.min()
				range.removeAll { Position pos -> pos.distanceAlt(poss[1]) > minDist }
				assert range.size() > 0
				current = range.min { Position pos -> getDangerValue(pos) }
				d += getDangerValue(current)
			}
			poss + d
		}
		pairs.min { it[2] }
	}

	/**
	 * Gets a danger value of the position.
	 *
	 * @param p position
	 * @return danger value
	 *
	 * @author Tomasz Dąbrowski
	 */
	public int getDangerValue(Position p) {
		dangerValues[p] ?: 0
	}

	/**
	 * Updates all danger values.
	 *
	 * @author Tomasz Dąbrowski
	 */
	public void updateDangerValues() {
		LogWindow.instance()?.Log("Updating danger values for team ${team.id}.");
		Pitch.getAllPositions().each { Position p ->
			dangerValues[p] = calculateDPValue(p, team.getId()) ?: 1
		}
		//PitchBoardGUI.instance?.refreshCells()
	}

	/**
	 * Calculates the danger value.
	 *
	 * @param position position
	 * @param teamID team id
	 * @return danger value
	 *
	 * @author Tomasz Dąbrowski
	 */
	public int calculateDPValue(Position position, TeamID teamID, List<Player> ignoredPlayers = []) {
		assert position
		Team oppositeTeam = teamID == TeamID.TEAM_A ? gameEngine.teamB() : gameEngine.teamA()
		assert oppositeTeam?.players()
		double danger = oppositeTeam.players().collect { Player p ->
			switch(p) {
				case { p.state() != PlayerState.IN_PLAY || p in ignoredPlayers }:
					0
					break
				case { position.distance(p) <= 1 }:
					NEAR_ENEMY_DANGER_VALUE
					break
				default:
					Math.pow(2, 2 - position.distanceAlt(p))
					break
			}
		}.max()
		if(danger >= NEAR_ENEMY_DANGER_VALUE) {
			(int) NEAR_ENEMY_DANGER_VALUE
		} else {
			(int)Math.ceil(danger * MAX_DANGER)
		}
	}
}

/**
 * Game thread class - every ai engine instance has it's own.
 *
 * @author Tomasz Dąbrowski
 *
 */
class GameThread extends Thread {

	/**
	 * AI engine.
	 */
	private AIEngine aiEngine

	/* (non-Javadoc)
	 * @see java.lang.Thread#run()
	 */
	@Override
	public void run() {
		setName("Game thread ${aiEngine.team.id}")
		synchronized (AIEngine.threadsLock) {
			try {
				IGameEngine gameEngine = aiEngine.gameEngine
				while (!gameEngine.isGameEnd() && !AIEngine.killFlag) {
					while(!gameEngine.gameEnd && gameEngine.currentTeamID() != aiEngine.team.id) {
						AIEngine.threadsLock.wait()
					}
					while(!gameEngine.gameEnd && System.nanoTime() - AIEngine.threadsTime < PitchGUI.getInterval() * 10.power(6)) {
						AIEngine.threadsLock.sleep(1)
					}
					boolean endTurn = false
					final GameState gameState = gameEngine.getGameState()
					if(gameEngine.isGameEnd()) {
						return
					}
					switch (gameState) {
						case {gameState == null }:
						case GameState.TEAM_PLACEMENT:
							aiEngine.setPlayer()
							break
						case GameState.TOUCH_BACK:
							gameEngine.onTouchBack(aiEngine.getTouchBackPlayer())
							break
						case [
							GameState.KICKING_TEAM,
							GameState.RECEIVING_TEAM
						]:
							endTurn = !aiEngine.playerAction()
							break
						default:
							break
					}
					if (endTurn && !aiEngine.isNextRound()) {
						gameEngine.endTurn()
					}
					AIEngine.threadsTime = System.nanoTime()
					AIEngine.threadsLock.notifyAll()
				}
			} catch(Exception ex) {
				System.out.println("Exception occured: $ex");
				AIEngine.setKillFlag(true);
			}
			AIEngine.threadsLock.notifyAll()
		}
	}
}